﻿using System;
using System.Collections;
using System.Collections.Generic;

namespace NRank.Common
{
	/// <summary>
	/// Creates collection wrapper which stores enumerated items into hashtable.
	/// </summary>
	/// <typeparam name="T">Type of items</typeparam>
	/// <typeparam name="TField">Type of hashed field</typeparam>
	public class HashedSource<T, TField> : IEnumerable<IRanked<T>>, IBoundHolder
	{
		/// <summary>
		/// Original items
		/// </summary>
		private readonly IEnumerable<IRanked<T>> source;

		/// <summary>
		/// Hashed member selector
		/// </summary>
		private readonly Func<T, TField> hashedValueSelector;

		/// <summary>
		/// Hashtable with seen items
		/// </summary>
		private readonly AssociativeFieldList<TField, IRanked<T>> seenItems = new AssociativeFieldList<TField, IRanked<T>>();

		/// <summary>
		/// Creates instance of HashedSource
		/// </summary>
		/// <param name="source">Original items</param>
		/// <param name="hashedValueSelector">Hashed value selector</param>
		public HashedSource(IEnumerable<IRanked<T>> source, Func<T, TField> hashedValueSelector)
		{
			this.source = source;
			this.hashedValueSelector = hashedValueSelector;

			this.MinRank = double.MaxValue;
			this.MaxRank = double.MaxValue;
		}

		/// <summary>
		/// Minimal seen rank
		/// </summary>
		public double MinRank { get; private set; }

		/// <summary>
		/// Maximal seen rank
		/// </summary>
		public double MaxRank { get; private set; }

		/// <summary>
		/// Current depth
		/// </summary>
		public int Depth { get; private set; }

		/// <summary>
		/// Indexer for seen items
		/// </summary>
		/// <param name="field">Value to count hash</param>
		/// <returns>Items with same field hash</returns>
		public IEnumerable<IRanked<T>> this[TField field]
		{
			get { return this.seenItems[field]; }
		}

		/// <summary>
		/// Current (last) seen item
		/// </summary>
		public IRanked<T> Current { get; private set; }

		/// <summary>
		/// Determines if the first item has been seen
		/// </summary>
		private bool hasReadFirstItem;

		/// <summary>
		/// Items enumeration
		/// </summary>
		/// <returns>Original items and stores them into hash table</returns>
		private IEnumerable<IRanked<T>> GetItems()
		{
			hasReadFirstItem = false;
			foreach (var item in source)
			{
				seenItems.Add(hashedValueSelector(item.Object), item);

				if (!hasReadFirstItem)
				{
					this.MaxRank = item.Rank;
					hasReadFirstItem = true;
				}

				this.MinRank = item.Rank;

				++this.Depth;

				this.Current = item;
				yield return item;
			}

			this.Current = null;
		}

		/// <summary>
		/// Items' enumerator
		/// </summary>
		/// <returns>Items' enumerator</returns>
		public IEnumerator<IRanked<T>> GetEnumerator()
		{
			return GetItems().GetEnumerator();
		}

		/// <summary>
		/// Nongeneric items' enumerator
		/// </summary>
		/// <returns>Nongeneric items' enumerator</returns>
		IEnumerator IEnumerable.GetEnumerator()
		{
			return GetEnumerator();
		}
	}
}